DialogflowのWebhookをAPI Gateway + Lambdaで作成し、Alexa-SDK(V2)みたいに書けるようにしてみた(TypeScriptで)
1 はじめに
Googleアシスタントでオリジナルの動作を実装するには、Dialogflow を使用します。
Dialogflowでは、通常、バックエンドの処理に、Cloud Functions for Firebase が使われますが、今回は、ここをAWS Lambda(以下、Lambda)及び、Amazon API Wategay(以下、API Wategay)で実装してみました。
最初に動作している様子です。
2 Alexa SDK風の実装
Dialogflow用のSDKであるActions on Google Client Library(actions-on-google) は、そのままではLambdaで使用することはできません。
actions-on-googleをLambdaで実行させるために actions-on-lambda というブリッジ実装もありましたが、 「すっかり慣れてきた、Alexa SDKと同じように書きたい!」 という身勝手で、ここはラッパーを自作することにしました。
次のコードは、自作したラッパーを使用して作成したコードです。既に Alexa SDK(V2) を使ったことがある方なら、ほとんど同じであることが感じて頂けるのでは無いでしょうか・・・
index.ts
import { GoogleCloudDialogflowV2WebhookRequest } from 'actions-on-google'; import { DialogFlow, HandlerInput, RequestHandler } from './DialogFlow'; // 自作ラッパー const CalcIntentHandler: RequestHandler = { canHandle(handlerInput: HandlerInput) { return handlerInput.requestEnvelope.intentName == 'CalcIntent';// CalcIntentを処理する }, handle(handlerInput: HandlerInput) { let params = handlerInput.requestEnvelope.parameters; const birthday = new Date(params.year, params.month - 1, params.day); // 誕生日 const today = new Date(); //今日 const ms = today.getTime() - birthday.getTime(); // 日付差(ミリ秒) const days = Math.floor(ms / (1000 * 60 * 60 *24)); // ミリ秒から日付へ変換 const speechText = '今日は、あなたが生まれてから、' + (days + 1) + '日目です'; return handlerInput.responseBuilder .speak(speechText) .withShouldEndSession(false) .getResponse(); } }; export async function handle(event: GoogleCloudDialogflowV2WebhookRequest, context: any) { const dialogFlow = new DialogFlow() .addRequestHandlers( CalcIntentHandler) .create(); return dialogFlow.invoke(event, context); }
3 DialogFlow.ts
DialogFlow.tsは、ちょうどAlexa SDKの部分を代替えするようなラッパーとなっています。
(1) RequestとResponse
actions-on-googleは、TypeScriptで作成されており、Dialogflowからのリクエストとレスポンスが型定義されいます。今回は、その型情報をそのまま利用させて頂きました。
Dialogflowは、最近V2となりましたが、V1とV2では、JSON形式が結構変わっているようです。しかし、この辺はSDKの型情報をそのまま使うことで安全に実装できそうです。
import { GoogleCloudDialogflowV2WebhookRequest } from 'actions-on-google'; import { GoogleCloudDialogflowV2WebhookResponse } from 'actions-on-google';
(2) ハンドラ
処理のメインとなるハンドラのインターフェースは、Alexa SDK(V2)と同じとしました。canHandler()が処理対象となるかどうかの判定で、handle()が実装の本体です。
ただし、レスポンスについては、actions-on-googleで定義されているGoogleCloudDialogflowV2WebhookResponseとしています。
export interface RequestHandler { canHandle(handlerInput: HandlerInput): Promise<boolean> | boolean; handle(handlerInput: HandlerInput): Promise<GoogleCloudDialogflowV2WebhookResponse> | GoogleCloudDialogflowV2WebhookResponse; }
(3) レスポンスビルダー
レスポンスビルダーは、現状、メソッドが下記の3つだけですが、使い方は、Alexa SDK(V2)と同じです。
speak(speechOutput: string): this withShouldEndSession(val: boolean): this getResponse(): GoogleCloudDialogflowV2WebhookResponse
export class ResponseBuilder { private response: GoogleCloudDialogflowV2WebhookResponse; constructor(){ this.response = { payload: { google: { expectUserResponse: true, richResponse: { items: [ { simpleResponse: { textToSpeech: '' } } ] } } } } } speak(speechOutput: string): this { if (this.response.payload && this.response.payload.google) { let google = this.response.payload.google if (google.richResponse && google.richResponse.items ) { let items = google.richResponse.items; if (items.length > 0) { if (items[0].simpleResponse) { items[0].simpleResponse.textToSpeech = speechOutput; } } } } return this; } withShouldEndSession(val: boolean): this { if( this.response.payload ) { if(this.response.payload.google ) { if(this.response.payload.google.expectUserResponse ) { this.response.payload.google.expectUserResponse = val; } } } return this; } getResponse(): GoogleCloudDialogflowV2WebhookResponse{ return this.response; } }
(4) リクエストの処理
Dialogflowからのリクエストは、RequestEnvelopeで処理されます。レクエストについては、ちょっとAlexaと差異が大きいので、インテント名やパラメータをコンストラクタでパースしてしまいました。
export class RequestEnvelope { intentName: string; query: string; parameters: ApiClientObjectMap<any>; userId: string; constructor(request: GoogleCloudDialogflowV2WebhookRequest) { if (request.queryResult){ if( request.queryResult.intent && request.queryResult.intent.displayName ) { this.intentName = request.queryResult.intent.displayName; } if( request.queryResult.queryText ) { this.query = request.queryResult.queryText; } if( request.queryResult.parameters ) { this.parameters = request.queryResult.parameters; } } if (request.responseId) { this.userId = request.responseId; } // console.log('IntentName: ' + this.intentName); // console.log('Query: ' + this.query); // console.log('Parameters: ' + JSON.stringify(this.parameters)); // console.log('UserId: ' + this.userId); } }
実装時は、下記のように、RequestEnvelopeから簡単に取り出せます。
let intentName = handlerInput.requestEnvelope.intentName; let params = handlerInput.requestEnvelope.parameters;
4 Dialogflowの定義
最後になりましたが、Dialogflowでの定義を簡単に紹介させて頂きます。
- Default Welcome Intentは、最初のメッセージを変更しただけです。
- インテントの定義は、CalcIntentだけで、生年月日を取得するようになっています。
- Fullfilmentでは、WebhookにAPI Gateway のEndpointを指定しています。
- Action on Googleでは、呼び出し名を「生まれて何日」とし、Actionsで先Dialogflowに繋いでいます。
5 最後に
今回は、DialogflowのWebhookをLambdaで実装し、Alexaのスキル風に書けるようにしてみました。
音声認識アプリの実装は、各社結構似たところがあるように思います。各社の技術を似た実装で乗り入れる仕組みを用意しておくと、もしかすると工数削減に有効かもしれません。
今回実装したコードは、下記に置きました。参考になれば幸いです。
※簡単に真似ただけですので、Alexa SDKを置き換えるようなものではないことを予めご了承ください。
[GitHub] https://github.com/furuya02/DialogflowOnLambda/